/**
 * Copyright (C) 2013 Daniel Gee.
 * See LICENSE.txt for more details.
 */
package com.rl.dungeon;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

import com.rl.dungeon.fov.VisionCheck;
import com.rl.dungeon.pathing.MoveCostMap;

/**
 * A Dungeon is a 3-dimensional block of {@link DungeonTerrain}, indexed 0,0,0 though x,y,z. When
 * subclassing this class, you have to provide the {@link #getLocationsAccessableFrom(Location)}
 * method yourself, which allows you to pick things like if the game allows for diagonals, if the
 * dungeon has multiple branches, and so on.
 * 
 * @author Daniel Gee
 */
public class Dungeon implements VisionCheck, MoveCostMap
{
	/**
	 * A Location that stores the edge of the dungeon.
	 */
	private final Location edge;

	/**
	 * The array storing all of the terrain of the dungeon. Access x,y,z
	 */
	private final DungeonTerrain[][][] tiles;

	/**
	 * Creates a new blank dungeon of the given size.
	 * 
	 * @param width
	 *            The width of the dungeon.
	 * @param height
	 *            The height of the dungeon.
	 * @param depth
	 *            The depth of the dungeon.
	 * @param defaultTileType
	 *            The default terrain of the new dungeon.
	 * @throws IllegalArgumentException
	 *             if <tt>width</tt>, <tt>height</tt>, or <tt>depth</tt> are less than 1.
	 */
	public Dungeon(int width, int height, int depth, DungeonTerrain defaultTileType)
	{
		if (width < 1 || height < 1 || depth < 1)
		{
			throw new IllegalArgumentException(String.format(
					"Width, Height, and Depth must all be greater than 0, got: %d,%d%,%d", width,
					height, depth));
		}
		this.tiles = new DungeonTerrain[width][height][depth];
		this.edge = Location.of(width - 1, height - 1, depth - 1);
		for (DungeonTerrain[][] yz : this.tiles)
		{
			for (DungeonTerrain[] z : yz)
			{
				Arrays.fill(z, defaultTileType);
			}
		}
	}

	/**
	 * Creates a new {@link Dungeon} by specifying the width, height, and depth in terms of a
	 * {@link Location} instance. The Location provided becomes the edge of the new dungeon.
	 * 
	 * @param edge
	 *            the edge of the new dungeon.
	 * @param defaultTileType
	 *            the default terrain of the new dungeon.
	 * @see #Dungeon(int, int, int, DungeonTerrain)
	 * @see #getEdge()
	 * @throws IllegalArgumentException
	 *             if any of the provided Location's coordinate components are less than 0.
	 */
	public Dungeon(Location edge, DungeonTerrain defaultTileType)
	{
		this(edge.x + 1, edge.y + 1, edge.z + 1, defaultTileType);
	}

	/**
	 * A location containing the maximum usable x, y, and z coordinate within this map. The minimum
	 * coordinate in all directions is 0.
	 * 
	 * @return the edge of the map.
	 */
	public Location getEdge()
	{
		return this.edge;
	}

	/**
	 * @see com.rl.dungeon.pathing.MoveCostMap#getLocationsAccessableFrom(com.rl.dungeon.Location)
	 */
	@Override
	public Collection<Location> getLocationsAccessableFrom(Location location)
	{
		Collection<Location> retVal = new ArrayList<Location>();
		for (Direction d : Direction.values())
		{
			if (d == Direction.None)
			{
				continue;
			}
			if (d == Direction.Up && this.getTileAt(location) != DungeonTerrain.StairsUp)
			{
				continue;
			}
			if (d == Direction.Down && this.getTileAt(location) != DungeonTerrain.StairsDown)
			{
				continue;
			}
			if (this.isWalkable(location.translate(d)))
			{
				retVal.add(location.translate(d));
			}
		}
		return retVal;
	}

	/**
	 * All movement costs default to 1.
	 * 
	 * @see com.rl.dungeon.pathing.MoveCostMap#getMoveCostAt(com.rl.dungeon.Location)
	 */
	@Override
	public int getMoveCostAt(Location location)
	{
		return 1;
	}

	/**
	 * Obtains the {@link DungeonTerrain} at the {@link Location} specified.
	 * 
	 * @param location
	 *            the location in question.
	 * @return the terrain at the location in question.
	 */
	public DungeonTerrain getTileAt(Location location)
	{
		if (location.x < 0 || location.y < 0 || location.z < 0 || location.x >= this.tiles.length
				|| location.y >= this.tiles[0].length || location.z >= this.tiles[0][0].length)
		{
			return DungeonTerrain.Void;
		}
		else
		{
			return this.tiles[location.x][location.y][location.z];
		}
	}

	/**
	 * @see com.rl.dungeon.pathing.MoveCostMap#isWalkable(com.rl.dungeon.Location)
	 */
	@Override
	public boolean isWalkable(Location location)
	{
		return this.getTileAt(location).isWalkable();
	}

	/**
	 * Assigns the {@link Location} the given {@link DungeonTerrain}.
	 * 
	 * @param location
	 *            the location to be changed.
	 * @param terrain
	 *            the new terrain type.
	 * @throws ArrayIndexOutOfBoundsException
	 *             if any of the <tt>location</tt> coordinates are less than 0 or greater than their
	 *             maximum respective coordinate as given by {@link #getEdge()}.
	 */
	public void setTileAt(Location location, DungeonTerrain terrain)
	{
		this.tiles[location.x][location.y][location.z] = terrain;
	}

	/**
	 * Determines vision blocking based on the {@link DungeonTerrain} at the location specified and
	 * the {@link DungeonTerrain#isVisionBlocking()} method of that terrain.
	 * 
	 * @see com.rl.dungeon.fov.VisionCheck#visionBlocked(com.rl.dungeon.Location)
	 */
	@Override
	public boolean visionBlocked(Location location)
	{
		return this.getTileAt(location).isVisionBlocking();
	}
}
